﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using P3SS.Security;
using System.Text.RegularExpressions;

namespace P3SS.Networking
{
	/// <summary>
	/// Performs interactions with a POP3 server
	/// </summary>
	public class Pop3Protocol : AbstractProtocol
	{
		#region Constructors
		/// <summary>
		/// Default Constructor. Set the POP3 server to localhost:110 with no username, password or encryption
		/// </summary>
		public Pop3Protocol()
		{
			server = "localhost";
			port = 110;
			username = "";
			password = "";
			UseAPOP = false;
			encryption = EncryptionType.None;
			serverCapabilities = "";
			CheckServerCapabilities = true;
		}

		/// <summary>
		/// Copy Constructor
		/// </summary>
		/// <param name="inPop3Protocol">Pop3Protocol to duplicate</param>
		public Pop3Protocol(Pop3Protocol inPop3Protocol)
		{
			server = inPop3Protocol.Server;
			port = inPop3Protocol.Port;
			username = inPop3Protocol.Username;
			password = inPop3Protocol.Password;
			UseAPOP = inPop3Protocol.UseAPOP;
			encryption = inPop3Protocol.Encryption;
			serverCapabilities = inPop3Protocol.ServerCapabilities;
			CheckServerCapabilities = inPop3Protocol.CheckServerCapabilities;
		}
		#endregion
		#region Properties
		/// <summary>
		/// Use APOP when logging in to the server?
		/// </summary>
		public bool UseAPOP { get; set; }

		/// <summary>
		/// Send the CAPA command to the server?
		/// </summary>
		public bool CheckServerCapabilities { get; set; }
		#endregion
		#region Public Methods
		/// <summary>
		/// Connects to server, encrypts connection and authenticates
		/// </summary>
		public void Connect()
		{
			string response = "";

			if (string.IsNullOrEmpty(server))
			{
				throw new ArgumentException("Server can not be blank", "server");
			}
			else if ((port < 1) || (port > 65535))
			{
				throw new ArgumentOutOfRangeException("port", "Port number is invalid"); 
			}
			else if (string.IsNullOrEmpty(username))
			{
				throw new ArgumentException("Username can not be blank", "username");
			}

			innerProtocol = new InnerProtocol(server, port, (encryption == EncryptionType.Implicit));
			response = innerProtocol.ReadLine();

			if (response.StartsWith("+OK"))
			{
				if (encryption == EncryptionType.Explicit)
				{
					string sslResponse = innerProtocol.WriteCommand("STLS");
					if (sslResponse.StartsWith("+OK"))
					{
						innerProtocol.UseSSL();
					}
					else
					{
						throw new ProtocolErrorException("Server rejected TLS request", sslResponse, "STLS");
					}
				}

				if (UseAPOP)
				{
					if ((response.Contains("<")) && (response.Contains(">")))
					{
						string timestamp = response.Substring(response.IndexOf("<"), response.IndexOf(">") - response.IndexOf("<") + 1);
						string digest = MD5Handler.getMd5Hash(timestamp + password);

						response = innerProtocol.WriteCommand("APOP " + username + " " + digest);

						if (!response.StartsWith("+OK"))
						{
							throw new ProtocolErrorException("APOP failed", response, "APOP " + username + " " + digest);
						}
					}
					else
					{
						throw new ProtocolErrorException("Server sent no timestamp. Are you sure it supports APOP", response, "");
					}
				}
				else
				{
					response = innerProtocol.WriteCommand("USER " + username);
					if (response.StartsWith("+OK"))
					{
						response = innerProtocol.WriteCommand("PASS " + password);
						if (response.StartsWith("+OK"))
						{
							if (CheckServerCapabilities)
							{
								response = innerProtocol.WriteCommand("CAPA", "\r\n.");
								if (response.StartsWith("+OK"))
								{
									serverCapabilities = response;
								}
								else
								{
									serverCapabilities = "Server responded incorrectly:\r\n" + response;
								}
							}
							else
							{
								serverCapabilities = "";
							}
						}
						else
						{
							if (response.Contains("invalid password"))
							{
								throw new ProtocolErrorException("Bad password", response, "PASS <password not shown>");
							}
							else
							{
								throw new ProtocolErrorException("Auth error after giving password", response, "PASS <password not shown>");
							}
						}
					}
					else
					{
						throw new ProtocolErrorException("USER not accepted", response, "USER " + username);
					}
				}
			}
			else
			{
				throw new ProtocolErrorException("Server responded to connection incorrectly", response, "");
			}
		}

		/// <summary>
		/// Gets the Unique IDs for all mail on the server
		/// </summary>
		public Dictionary<int, string> GetUids()
		{
			Regex uidRegex = new Regex("([0-9]+) ([!-~]+)");
			Dictionary<int, string> output = new Dictionary<int, string>();

			string response = innerProtocol.WriteCommand("UIDL", "\r\n.");

			if (response.StartsWith("+OK"))
			{
				MatchCollection uidMatches = uidRegex.Matches(response);

				foreach (Match uidMatch in uidMatches)
				{
					output.Add(Int32.Parse(uidMatch.Groups[1].Value), uidMatch.Groups[2].Value);
				}
			}
			else
			{
				throw new ProtocolErrorException("Failed to get unique ids", response, "UIDL");
			}

			return output;
		}

		/// <summary>
		/// Gets the number of messages waiting on the server
		/// </summary>
		public int GetNumberMailWaiting()
		{
			Regex numMessagesRegex = new Regex(@"\+OK ([0-9]+) ([0-9]+)");
			int numMessages = 0;

			string response = innerProtocol.WriteCommand("STAT");

			if (response.StartsWith("+OK"))
			{
				Match numMessagesMatch = numMessagesRegex.Match(response);

				if (numMessagesMatch.Success)
				{
					numMessages = Int32.Parse(numMessagesMatch.Groups[1].Value);
				}
				else
				{
					throw new ProtocolErrorException("Failed to extract number of messages", response, "STAT");
				}
			}
			else
			{
				throw new ProtocolErrorException("Failed to get number of messages", response, "STAT");
			}

			return numMessages;
		}

		/// <summary>
		/// Gets a single mail item from the server
		/// </summary>
		/// <param name="mailNumber">The mail item number (not Unique ID)</param>
		public string GetMailItem(int mailNumber)
		{
			string response = innerProtocol.WriteCommand("RETR " + mailNumber.ToString(), "\r\n.");
			if (response.StartsWith("+OK"))
			{
				response = response.Substring(response.IndexOf("\n") + 1);
			}
			else
			{
				throw new ProtocolErrorException("Failed to get mail item", response, "RETR " + mailNumber.ToString());
			}

			return response;
		}

		/// <summary>
		/// Marks a single mail item for removal
		/// </summary>
		/// <param name="mailNumber">The mail item number (not Unique ID)</param>
		public void DeleteMailItem(int mailNumber)
		{
			string response = innerProtocol.WriteCommand("DELE " + mailNumber.ToString());
			if (!response.StartsWith("+OK"))
			{
				throw new ProtocolErrorException("Failed to delete mail item", response, "DELE " + mailNumber.ToString());
			}
		}

		/// <summary>
		/// Closes the connection
		/// </summary>
		public void Disconnect()
		{
			string response = innerProtocol.WriteCommand("QUIT");
			innerProtocol.Close();

			if (!response.StartsWith("+OK"))
			{
				throw new ProtocolErrorException("Server refused to QUIT. Disconnected connection anyway", response, "QUIT");
			}
		}

		/// <summary>
		/// Checks that all settings are valid
		/// </summary>
		/// <returns>True if all settings are valid, otherwise false</returns>
		public override bool ValidateSettings()
		{
			bool isValid = true;

			isValid &= !string.IsNullOrEmpty(username);
			isValid &= base.ValidateSettings();

			return isValid;
		}

		/// <summary>
		/// Compares this Pop3Protocol to another Pop3Protocol
		/// </summary>
		/// <param name="obj">Pop3Protocol to compare</param>
		/// <returns>True if all properties are equal, otherwise false</returns>
		public override bool Equals(object obj)
		{
			bool isEquals = false;

			if (obj is Pop3Protocol)
			{
				Pop3Protocol otherProtocol = (Pop3Protocol)obj;

				if ((UseAPOP == otherProtocol.UseAPOP) && (base.Equals(obj)))
				{
					isEquals = true;
				}
			}

			return isEquals;
		}

		/// <summary>
		/// Creates a unique ID for this object
		/// </summary>
		/// <returns></returns>
		public override int GetHashCode()
		{
			return base.GetHashCode();
		}
		#endregion
	}
}
